awspecでAWSリソースをテストする
こんにちは、佐伯です。
AWSリソースの設定をマネジメントコンソールから目視確認するのはつらみがありますよね。ということで、今日はAWSリソースをテストするツール、awspecについて紹介したいと思います。
awspecとは
awspecはAWSのリソース構成をServerspecのようにテストできるツールです。GitHubリポジトリは下記になります。
GitHub - k1LoW/awspec: RSpec tests for your AWS resources.
ローカル環境を整える
rbenv, Bundler, direnvの話になるので、普段使ってる方は省略してください。
OSSを利用する際、バージョン管理が重要な課題になってきます。注目度が高いOSSほど頻繁にバージョンアップされ、新たな機能追加やバグ修正が行われます。また、稀にバージョンアップによって意図せぬバグが生まれ、以前のバージョンではできていたことができなくなってしまうこともあります。
そのため、既存のコードが動作するかをバージョンアップのタイミングで確認せずに安易にバージョンアップしてしまうと、ハマってしまうことがありますので、rbenvでRubyのバージョンを、Bundlerでgemのバージョンを管理できるように、ローカル環境を整えましょう。
なお、その後CIを導入する場合でも同じ仕組みを使うことができると思いますので、ちゃんと取り組みたいポイントになります。
rbenvのインストール
といいつつも、過去エントリのご紹介になります。下記エントリを参考にrbenvをインストールして下さい。rbenvは複数のRubyバージョンをディレクトリごとなどの単位で切り替えることができます。
様々な言語の実行環境が必要な方は下記エントリを参考にanyenvをインストールしても良いと思います。
Bundlerのインストール
こちらも過去エントリ(というか↑の続き)の紹介になります。Bundlerでgemのバージョン管理を行うのでインストールします。
(必要に応じて)direnvのインストール
複数のAWSアカウントを使用している場合は、AWSアカウント毎のIAMユーザーのアクセスキー/シークレットキーを使用するのではなく、direnvを使用してAssumeRoleで一時クレデンシャルを環境変数にセットする方法がおすすめです。
セットアップ
インストール手順はGitHub - k1LoW/awspec: RSpec tests for your AWS resources.に記載のとおりですが、上記のツール類を使った場合のセットアップ方法を書きます。
Rubyのインストール
awspecはRuby 2.1以上が必要です。Ruby 2.4.3までテスト済みのようなので、今回はRuby 2.4.3をインストールします。
$ rbenv install 2.4.3 $ rbenv local 2.4.3 $ rbenv versions system * 2.4.3 (set by /Users/saiki.ko/example/.ruby-version)
Bundlerのインストール
私の環境にRuby 2.4.3を初めて入れたので、Bundlerもインストールしておきます。
$ gem install bundler
awspecのインストール
Gemfileを生成した後にawspecとrake(後ほど実行するawspec init
でRakeタスク(Rakefile)を生成してくれるので)を追記し、bundle installを実行してawspecとrakeをインストールします。
$ bundle init Writing new Gemfile to /Users/saiki.ko/example/Gemfile $ echo 'gem "awspec"' >>Gemfile $ echo 'gem "rake"' >>Gemfile $ bundle install --path vendor/bundle Fetching gem metadata from https://rubygems.org/.................... Resolving dependencies... Fetching aws-partitions 1.57.0 Installing aws-partitions 1.57.0 Fetching aws-sigv4 1.0.2 Installing aws-sigv4 1.0.2 Fetching jmespath 1.3.1 Installing jmespath 1.3.1 Fetching aws-sdk-core 3.14.0 Installing aws-sdk-core 3.14.0 ......(省略)......
awspecディレクトリとinitファイルの作成
awspecのコードはawspecディレクトリで管理したい(後ほどTerraformのコードも作成予定)ので、ディレクトリを作成し、awspec init
コマンドでinitファイルを生成します。
$ mkdir awspec $ cd awspec $ bundle exec awspec init + spec/ + spec/spec_helper.rb + Rakefile + spec/.gitignore + .rspec
認証情報の設定
AWS CLIの認証情報にも対応してますし、spec/secrets.ymlに設定してもよいですし、紹介したdirenvを使って環境変数にセットする形でもよいです。また、IAMの権限は基本的にはRead権限があれば大抵のテストは実行できると思います。
動かしてみる
awspecを使ってAWSリソースをテストしてみましょう。
その前に
そもそもテストするAWSリソースがないので、さくっとTerraformで作っちゃいます。terraformディレクトリを作成し、その中にTerraform Module Refistryのvpc/awsモジュールを使ったvpc.tfを作成し、VPC、サブネット、ルートテーブル、インターネットゲートウェイを作成しました。
provider "aws" { region = "ap-northeast-1" } module "vpc" { source = "terraform-aws-modules/vpc/aws" name = "my-vpc" cidr = "10.0.0.0/16" azs = ["ap-northeast-1a", "ap-northeast-1c"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24"] enable_nat_gateway = false enable_vpn_gateway = false tags = { Terraform = "true" Environment = "dev" } }
テストコードを書く
現在のディレクトリ構成は下記のとおりです。awspec/spec配下にテストコードを作成します。
example ├── Gemfile ├── Gemfile.lock ├── awspec │ ├── Rakefile │ └── spec ├── terraform │ └── vpc.tf └── vendor └── bundle
基本的にはドキュメントを読みながらリソースごとにテストする項目を決め、*_spec.rbというファイル名でテストを書きます。特にリソース単位でファイルを分ける必要はないですが、私はなんとなく分けてます。
また、spec/spec_helper.rbでawspecをロードしてるので、各テストコードファイルの先頭にrequire 'spec_helper'
を書きましょう。
VPC
require 'spec_helper' describe vpc('my-vpc') do it { should exist } it { should be_available } its(:cidr_block) { should eq '10.0.0.0/16' } its(:dhcp_options_id) { should eq 'dopt-XXXXXXXX' } it { should have_route_table('my-vpc-private-ap-northeast-1a') } it { should have_route_table('my-vpc-private-ap-northeast-1c') } it { should have_route_table('my-vpc-public') } it { should have_network_acl('acl-XXXXXXXX') } it { should have_tag('Name').value('my-vpc') } it { should have_tag('Environment').value('dev') } end
Subnet
require 'spec_helper' describe subnet('my-vpc-private-ap-northeast-1a') do it { should exist } it { should be_available } it { should belong_to_vpc('my-vpc') } its(:availability_zone) { should eq 'ap-northeast-1a' } its(:cidr_block) { should eq '10.0.1.0/24'} its(:map_public_ip_on_launch) { should eq false } it { should have_tag('Name').value('my-vpc-private-ap-northeast-1a') } it { should have_tag('Environment').value('dev') } end describe subnet('my-vpc-private-ap-northeast-1c') do it { should exist } it { should be_available } it { should belong_to_vpc('my-vpc') } its(:availability_zone) { should eq 'ap-northeast-1c' } its(:cidr_block) { should eq '10.0.2.0/24'} its(:map_public_ip_on_launch) { should eq false } it { should have_tag('Name').value('my-vpc-private-ap-northeast-1c') } it { should have_tag('Environment').value('dev') } end describe subnet('my-vpc-public-ap-northeast-1a') do it { should exist } it { should be_available } it { should belong_to_vpc('my-vpc') } its(:availability_zone) { should eq 'ap-northeast-1a' } its(:cidr_block) { should eq '10.0.101.0/24'} its(:map_public_ip_on_launch) { should eq true } it { should have_tag('Name').value('my-vpc-public-ap-northeast-1a') } it { should have_tag('Environment').value('dev') } end describe subnet('my-vpc-public-ap-northeast-1c') do it { should exist } it { should be_available } it { should belong_to_vpc('my-vpc') } its(:availability_zone) { should eq 'ap-northeast-1c' } its(:cidr_block) { should eq '10.0.102.0/24'} its(:map_public_ip_on_launch) { should eq true } it { should have_tag('Name').value('my-vpc-public-ap-northeast-1c') } it { should have_tag('Environment').value('dev') } end
Internet Gataway
require 'spec_helper' describe internet_gateway('my-vpc') do it { should exist } it { should be_attached_to('my-vpc') } it { should have_tag('Name').value('my-vpc') } it { should have_tag('Environment').value('dev') } end
Route Table
require 'spec_helper' describe route_table('my-vpc-private-ap-northeast-1a') do it { should exist } it { should have_subnet('my-vpc-private-ap-northeast-1a') } it { should have_route('10.0.0.0/16').target(gateway: 'local') } it { should have_tag('Name').value('my-vpc-private-ap-northeast-1a') } it { should have_tag('Environment').value('dev') } end describe route_table('my-vpc-private-ap-northeast-1c') do it { should exist } it { should have_subnet('my-vpc-private-ap-northeast-1c') } it { should have_route('10.0.0.0/16').target(gateway: 'local') } it { should have_tag('Name').value('my-vpc-private-ap-northeast-1c') } it { should have_tag('Environment').value('dev') } end describe route_table('my-vpc-public') do it { should exist } it { should have_subnet('my-vpc-public-ap-northeast-1a') } it { should have_subnet('my-vpc-public-ap-northeast-1c') } it { should have_route('10.0.0.0/16').target(gateway: 'local') } it { should have_route('0.0.0.0/0').target(gateway: 'my-vpc') } it { should have_tag('Name').value('my-vpc-public') } it { should have_tag('Environment').value('dev') } end
テストを実行する
awspec init
でRakefileが生成されており、awspecディレクトリ配下でbundle exec rake spec
でテストが実行できます。
[saiki.ko@~/example/awspec] $ bundle exec rake spec /Users/saiki.ko/.anyenv/envs/rbenv/versions/2.4.3/bin/ruby -I/Users/saiki.ko/example/vendor/bundle/ruby/2.4.0/gems/rspec-core-3.7.1/lib:/Users/saiki.ko/example/vendor/bundle/ruby/2.4.0/gems/rspec-support-3.7.1/lib /Users/saiki.ko/example/vendor/bundle/ruby/2.4.0/gems/rspec-core-3.7.1/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb internet_gateway 'my-vpc' should exist should be attached to "my-vpc" should have tag "Name" should have tag "Environment" route_table 'my-vpc-private-ap-northeast-1a' should exist should have subnet "my-vpc-private-ap-northeast-1a" should have route "10.0.0.0/16" should have tag "Name" should have tag "Environment" route_table 'my-vpc-private-ap-northeast-1c' should exist should have subnet "my-vpc-private-ap-northeast-1c" should have route "10.0.0.0/16" should have tag "Name" should have tag "Environment" route_table 'my-vpc-public' should exist should have subnet "my-vpc-public-ap-northeast-1a" should have subnet "my-vpc-public-ap-northeast-1c" should have route "10.0.0.0/16" should have route "0.0.0.0/0" should have tag "Name" should have tag "Environment" subnet 'my-vpc-private-ap-northeast-1a' should exist should be available should belong to vpc "my-vpc" should have tag "Name" should have tag "Environment" availability_zone should eq "ap-northeast-1a" cidr_block should eq "10.0.1.0/24" map_public_ip_on_launch should eq false subnet 'my-vpc-private-ap-northeast-1c' should exist should be available should belong to vpc "my-vpc" should have tag "Name" should have tag "Environment" availability_zone should eq "ap-northeast-1c" cidr_block should eq "10.0.2.0/24" map_public_ip_on_launch should eq false subnet 'my-vpc-public-ap-northeast-1a' should exist should be available should belong to vpc "my-vpc" should have tag "Name" should have tag "Environment" availability_zone should eq "ap-northeast-1a" cidr_block should eq "10.0.101.0/24" map_public_ip_on_launch should eq true subnet 'my-vpc-public-ap-northeast-1c' should exist should be available should belong to vpc "my-vpc" should have tag "Name" should have tag "Environment" availability_zone should eq "ap-northeast-1c" cidr_block should eq "10.0.102.0/24" map_public_ip_on_launch should eq true vpc 'my-vpc' should exist should be available should have route table "my-vpc-private-ap-northeast-1a" should have route table "my-vpc-private-ap-northeast-1c" should have route table "my-vpc-public" should have network acl "acl-XXXXXXXX" should have tag "Name" should have tag "Environment" cidr_block should eq "10.0.0.0/16" dhcp_options_id should eq "dopt-XXXXXXXX" Finished in 2.05 seconds (files took 1.34 seconds to load) 63 examples, 0 failures
テキストじゃあんまりパッとしないので、画像で一部分を貼ります。
ちなみに、テストが失敗した時はこんな感じ。(VPCのNameタグのテストを'my-vpc'から'my-vpn'に変更して意図的にテストを失敗させてます)
ファイル単体でテストを実行したい場合は、bundle exec rspec spec/<ファイル名>
で実行できます。
[saiki.ko@~/example/awspec] $ bundle exec rspec spec/vpc_spec.rb vpc 'my-vpc' should exist should be available should have route table "my-vpc-private-ap-northeast-1a" should have route table "my-vpc-private-ap-northeast-1c" should have route table "my-vpc-public" should have network acl "acl-XXXXXXXX" should have tag "Name" should have tag "Environment" cidr_block should eq "10.0.0.0/16" dhcp_options_id should eq "dopt-XXXXXXXX" Finished in 0.61097 seconds (files took 1.37 seconds to load) 10 examples, 0 failures
テストコードを生成する
awspecにはテストコードを生成するgenerateサブコマンドがあります。awspec v1.3.0でテストコードを生成できるリソースは下記の通りです。
$ bundle exec awspec generate --help Commands: awspec generate acm # Generate acm spec awspec generate alb [vpc_id] # Generate alb spec from VPC ID (or VPC "Name" tag) awspec generate cloudwatch_alarm # Generate cloudwatch_alarm spec awspec generate cloudwatch_event # Generate cloudwatch_event spec awspec generate cloudwatch_logs # Generate cloudwatch_logs spec awspec generate directconnect # Generate directconnect spec awspec generate ebs # Generate attached ebs spec awspec generate ec2 [vpc_id] # Generate ec2 spec from VPC ID (or VPC "Name" tag) awspec generate efs # Generate efs spec awspec generate eip # Generate eip spec awspec generate elasticsearch # Generate elasticsearch spec awspec generate elb [vpc_id] # Generate elb spec from VPC ID (or VPC "Name" tag) awspec generate help [COMMAND] # Describe subcommands or one specific subcommand awspec generate iam_group # Generate iam_group spec awspec generate iam_policy # Generate attached iam_policy spec awspec generate iam_role # Generate iam_role spec awspec generate iam_user # Generate iam_user spec awspec generate internet_gateway [vpc_id] # Generate internet_gateway spec from VPC ID (or VPC "Name" tag) awspec generate kms # Generate kms spec awspec generate lambda # Generate lambda spec awspec generate nat_gateway [vpc_id] # Generate nat_gateway spec from VPC ID (or VPC "Name" tag) awspec generate network_acl [vpc_id] # Generate network_acl spec from VPC ID (or VPC "Name" tag) awspec generate network_interface [vpc_id] # Generate network_interface spec from VPC ID (or VPC "Name" tag) awspec generate nlb [vpc_id] # Generate nlb spec from VPC ID (or VPC "Name" tag) awspec generate rds [vpc_id] # Generate rds spec from VPC ID (or VPC "Name" tag) awspec generate rds_db_cluster_parameter_group [paramater_name] # Generate rds_db_cluster_parameter_group spec from paramater name. awspec generate rds_db_parameter_group [paramater_name] # Generate rds_db_parameter_group spec from paramater name. awspec generate route53_hosted_zone [example.com.] # Generate route53_hosted_zone spec from Domain name awspec generate route_table [vpc_id] # Generate route_table spec from VPC ID (or VPC "Name" tag) awspec generate s3_bucket [bucket_name] # Generate s3_bucket spec from S3 bucket name. if NO args, Generate all. awspec generate security_group [vpc_id] # Generate security_group spec from VPC ID (or VPC "Name" tag) awspec generate subnet [vpc_id] # Generate subnet spec from VPC ID (or VPC "Name" tag) awspec generate vpc [vpc_id] # Generate vpc spec from VPC ID (or VPC "Name" tag)
例えば、今回作成したVPC(Name:my-vpc)のテストは以下のコマンドで生成できます。生成されたテストコードは標準出力に出力されるので、リダイレクトなりして*_spec.rbファイルに書きます。なお、生成したテストコードを使用する際、require 'spec_helper'
を書くのを忘れがちになるので注意しましょー。
$ bundle exec awspec generate vpc my-vpc describe vpc('my-vpc') do it { should exist } it { should be_available } its(:vpc_id) { should eq 'vpc-XXXXXXXX' } its(:cidr_block) { should eq '10.0.0.0/16' } it { should have_route_table('rtb-XXXXXXXX') } it { should have_route_table('my-vpc-private-ap-northeast-1a') } it { should have_route_table('my-vpc-private-ap-northeast-1c') } it { should have_route_table('my-vpc-public') } it { should have_network_acl('acl-XXXXXXXX') } end
全てのリソースに対応しているわけではない
AWSは新しいサービスや機能がガンガンリリースされているので、全てのAWSリソースに対応しているわけではありません。「これテストしたいけど対応してないなー」なんて思ったら、コントリビュートチャンスです。
まとめ
簡単にawspecの使い方を紹介させて頂きました。CloudFormationやTerraformなどの構成管理ツールでAWS環境を構築しているのだから、テストは不要といった意見もありますが、運用していく中であるべき姿に保たれているか?をチェックすることは大事だと思います。GitリポジトリとCodeBuildやCircleCIなどのサービスを連動させて自動テストしたり、定期的に自動テストしたりできるといい感じになるので、以下エントリも参考にして頂ければと思います。